踩坑集锦
1.1 No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuth
有可能的原因是重写了configure方法,而方法体内是空的。这是一种非常傻逼的操作…
1.2 java.lang.IllegalStateException: UserDetailsService is required.
出现这种错误的原因可能是在配置文件中配置了用户名和密码…,而且用户名和密码是可以进行登陆的。不过后台会报错,只需将配置文件中的用户名密码配置在config方法中即可。
1 |
|
1. Remember-Me的使用
如果需要实现自动登陆
的功能,且在关闭浏览器后重开,或者重启服务器后还能自动登陆,可以使用Spring Security的Remember-me功能。只需要配置一下rememberMe()即可。
1 |
|
当配置rememberMe()后,自动登陆功能就可以实现了:
在登陆页面,会出现Remember me on this computer.的选项。如果勾选了这个选项,发送POST请求的/login会在body中增加一个remember-me:on的参数。
如果是自定义的登陆页面,想使用remember-me的功能,传递的key值就应该是remember-me
.当勾选了后,再次访问其他接口,Cookie中会携带remember-me:
1 | Cookie: JEECGINDEXSTYLE=ace;JSESSIONID=93573581FD0676F64B7D495F9968D6AE; remember-me=dXNlcjoxNjE1OTYzNzQ0Mzk5OjhiYTBhZjkxYzlhNzBhYjlkMzQwNGMyNWM2OWFhYzdk |
接下来,我们来研究一下remember-me的组成,这串字符串是经过Base64编码后的,我们写段小代码来还原它的真面目:
1 |
|
输出结果为:分割的字符串,其中:
- 第一段为
用户名
,也就是在登陆界面登陆的用户名 - 第二段为
过期时间
,默认是两周 - 第三段为
MD5计算的散列值
。他的明文格式是username + ":" + tokenExpiryTime + ":" + password + ":" + key
,最后的 key 是一个散列盐值,可以用来防治令牌被修改
1 | user:1615962766412:d21bd6b9b67b342dd7e0f232917aff4e |
那么,如果用户勾选了Remember-me的选项后,登陆流程是这样子的:
用户勾选remember-me选项后,/login请求会带上remember-on的参数。在浏览器关闭后,或服务器重启后,用户再去访问接口,此时会携带Cookie中的remember-me到服务端。服务端拿到这个remember-me,就可以解析用户名和过期时间,再根据用户名查询到用户密码,然后通过MD5散列函数计算出散列值,将计算出的散列值与浏览器传递的散列值进行对比,就能确认这个令牌是否有效。
2. Remember-Me的生成与校验
2.1 生成过程
按照remember-me的格式,生成的过程需要的有用户名
,过期时间
,密码(MD5加密中使用)
。
remember-me参数的生成是在TokenBasedRememberMeServices类中的onLoginSuccess方法中:
1 | public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { |
makeTokenSignature使用MD5生成散列值,可以看到散列值的明文格式。
同时也说明了MD5加密使用MessageDigest
,也要注意使用Hex.encode进行编码,否则输出是乱码。
1 | protected String makeTokenSignature(long tokenExpiryTime, String username, String password) { |
散列函数中的key如果没有设置,默认是在 RememberMeConfigurer#getKey 方法中进行设置的,它的值是一个 UUID 字符串。
1 | private String getKey() { |
由于我们自己没有设置 key,key 默认值是一个 UUID 字符串,这样会带来一个问题,就是如果服务端重启,这个 key 会变,这样就导致之前派发出去的所有 remember-me 自动登录令牌失效(测试好像并不会???
),所以,我们可以指定这个 key。指定方式如下:
1 |
|
2.2 校验过程
RememberMeAuthenticationFilter#doFilter方法:
1 | private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
AbstractRememberMeServices#autoLogin:
真正校验remember-me参数的方法是processAutoLoginCookie。它是一个抽象方法,实现有2个:
- TokenBasedRememberMeServices
- PersistentTokenBasedRememeberMeServices
这里使用的是TokenBasedRememberMeServices。而PersistentTokenBasedRememeberMeServices是持久化令牌使用的,后续会介绍。
1 | public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { |
可以看到,核心就是提取出 cookie 信息,并对 cookie 信息进行解码,解码之后,再调用 processAutoLoginCookie 方法去做校验。
3. 风险以及解决方法
如果我们开启了 RememberMe 功能,最最核心的东西就是放在 cookie 中的令牌了,这个令牌突破了 session 的限制,即使服务器重启、即使浏览器关闭又重新打开,只要这个令牌没有过期,就能访问到数据。
一旦令牌丢失,别人就可以拿着这个令牌随意登录我们的系统了,这是一个非常危险的操作。
但是实际上这是一段悖论,为了提高用户体验(少登录),我们的系统不可避免的引出了一些安全问题,不过我们可以通过技术将安全风险降低到最小。
3.1 持久化令牌
持久化令牌就是在基本的自动登录功能基础上,又增加了新的校验参数,来提高系统的安全性,这一些都是由开发者在后台完成的,对于用户来说,登录体验和普通的自动登录体验是一样的。
在持久化令牌中,新增了两个经过 MD5 散列函数计算的校验参数,一个是 series,另一个是 token。其中,series 只有当用户在使用用户名/密码登录时,才会生成或者更新,而 token 只要有新的会话,就会重新生成,这样就可以避免一个用户同时在多端登录,就像手机 QQ ,一个手机上登录了,就会踢掉另外一个手机的登录,这样用户就会很容易发现账户是否泄漏。
要想使用持久化令牌,我们就需要一张表来保存令牌。可以自定义表,也可以使用Spring Security提供的JdbcTokenRepositoryImpl。根据JdbcTokenRepositoryImpl定义的操作sql,创建表的sql如下:
1 | CREATE TABLE `persistent_logins` ( |
用来保存令牌的处理类是PersistentRememberMeToken。
1 | public class PersistentRememberMeToken { |
此外,我们还需要添加JDBC和Mysql的依赖。并在配置文件中配置数据库相关的属性:
1 | spring: |
Spring Security的配置类也需要添加JdbcRepositoryImpl和tokenRepository:
1 |
|
访问流程和使用RememberMe的时候没有什么不同(用户的体验是一样的)。唯一不同的是持久化令牌的方式把remember-me保存到了数据库中。
前端传递的参数remember-me经过Base64解码后:
1 | uJQ60QhTdmucg5jnmPjhDg%3D%3D:tuQyXHvjgimTtFI7jLnQgw%3D%3D |
%3D
代表=
。此时查看数据库中的表,多了一条记录:
| series| token |
|–|–|
| uJQ60QhTdmucg5jnmPjhDg== | tuQyXHvjgimTtFI7jLnQgw==|
数据库中的记录和我们看到的 remember-me 令牌解析后是一致的。
3.2 持久化令牌生成流程
持久化令牌的生成流程在PersistentTokenBasedRememberMeServices#onLoginSuccess:
与TokenBasedRememberMeServices不同的是,PersistentTokenBasedRememberMeServices不需要获取用户的密码,series和token都是调用SecureRandom 随机生成的。不同于我们以前用的 Math.random 或者 java.util.Random 这种伪随机数,SecureRandom 则采用的是类似于密码学的随机数生成规则,其输出结果较难预测,适合在登录这样的场景下使用。
1 |
|
3.3 持久化令牌校验流程
持久化令牌的校验核心在PersistenctTokenBasedRememberMeServices#processAutoLoginCookie:
1 |
|
3.4 二次校验
为了让用户使用方便,我们开通了自动登录功能,但是自动登录功能又带来了安全风险,一个规避的办法就是如果用户使用了自动登录功能,我们可以只让他做一些常规的不敏感操作,例如数据浏览、查看,但是不允许他做任何修改、删除操作,如果用户点击了修改、删除按钮,我们可以跳转回登录页面,让用户重新输入密码确认身份,然后再允许他执行敏感操作。
1 |
|
- /remember接口是需要 rememberMe 才能访问。
- /admin 是需要 fullyAuthenticated,fullyAuthenticated 不同于 authenticated,fullyAuthenticated 不包含自动登录的形式,而 authenticated 包含自动登录的形式。
- /hello是 authenticated 就能访问,也就是账号密码登陆和自动登陆都可以。